package org.erikaredmark.monkeyshines;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.common.collect.Lists;
/**
*
* Represents an underlying concept of the high scores for the session. This object keeps high score records
* ordered from highest to lowest and stores the names of the achievers. A single instance is created when loading
* a highscores file and typically exist until the game is closed (making an instance of this object
* global in a sense). The high scores object can be added to and persisted to a file (typically the
* preferences file given the simplicity of the object.
* <p/>
* Only ten scores are stored. Adding a new score may either not work if the score is too low, or bump another
* score out.
* <p/>
* Since this object
*
* @author Erika Redmark
*
*/
public final class HighScores {
private static final String CLASS_NAME = "org.erikaredmark.monkeyshines.HighScores";
private static final Logger LOGGER = Logger.getLogger(CLASS_NAME);
// primitive array since size must be enforced
private HighScore[] scores = new HighScore[MAX_SCORES];
private int scoreSize = 0;
private static final int MAX_SCORES = 10;
public HighScores() { }
public HighScores(List<HighScore> initialScores) {
if (initialScores.size() > MAX_SCORES) {
LOGGER.warning("More scores in file than maximum (10). Only first " + MAX_SCORES + " scores from the top of the file will be used");
initialScores = Lists.partition(initialScores, MAX_SCORES).get(0);
}
// Do not copy or order may not be preserved.
// Score size will be handled automatically by addScore, do NOT set it here.
for (HighScore s : initialScores) {
addScore(s.name, s.score);
}
}
/**
*
* Checks if the score is higher than the lowest score. If not, the score cannot be added.
*
* @param score
*
* @return
* {@code true} if the score can be entered, {@code false} if otherwise.
*
*/
public boolean isScoreHigh(int score) {
if (scoreSize < 10) return true;
// We can assume ordered array, so look at last element
return scores[9].score < score;
}
/**
*
* Adds the given score to the high scores list for the given person. This does not automatically persist
* the scores to a preferences file.
* <p/>
* If the score is too low and can't be added an exception is thrown. Only call after confirming with
* {@code isScoreHigh(int)} first.
*
* @param name
* name of the person who got the score. Commas will be removed (as they are internal delimiters)
*
* @param score
*
* @throws IllegalArgumentException
* if score is too low to be added. Call {@code isScoreHigh(int) } first
*
*/
public void addScore(String name, int score) {
if (!(isScoreHigh(score) ) ) {
throw new IllegalArgumentException("Score " + score + " is too low to be entered into the high scores. Check with isScoreHigh first");
}
name = name.replace(',', ' ');
// Condition 1: Bump out the lowest score since there isn't space left. We will resort array later.
if (scoreSize == 10) {
scores[9] = new HighScore(name, score);
// Condition 2: Enough room
} else {
scores[scoreSize] = new HighScore(name, score);
++scoreSize;
}
Arrays.sort(scores, 0, scoreSize);
}
/**
*
* Returns the name/score pairs in this object. The list is only as long as the number of actual
* entries and therefore may be less than 10.
* <p/>
* Changes to the returned list will not affect this object
*
* @return
* copy of the high scores list in this object
*
*/
public List<HighScore> getHighScores() {
List<HighScore> highs = new ArrayList<>(scoreSize);
for (int i = 0; i < scoreSize; ++i) {
highs.add(scores[i]);
}
return highs;
}
/**
*
* Persists the high scores data as a basic list written out to a text file. The path should NOT be
* the preferences file.
*
* @param scoresList
* path to high scores list. File will be created if one does not exist
*
* @return
* {@code true} if the high scores could be saved, {@code false} if otherwise
*
*/
public boolean persistScores(Path highScores) {
try {
writeScores(highScores, scores, scoreSize);
return true;
} catch (IOException e) {
return false;
}
}
/** Reads the high score list from a basic plain-text input. Each line is a name, delimiter, and score.
* Delimiter is defined as a comma.
* @param scoreFile the input file
* @return list of high scores from file.
* @throws IOException if the file could not be read. Ensure it exists.
*/
private static List<HighScore> readScores(Path scoreFile) throws IOException {
List<HighScore> highReturns = new ArrayList<>();
try (BufferedReader reader = Files.newBufferedReader(scoreFile, Charset.forName("UTF-8") ) ) {
String scoreLine = null;
while ( (scoreLine = reader.readLine() ) != null) {
String[] parts = scoreLine.split("\\,");
// Ignore bad lines; the user can modify the file so let's try to be as liberal as possible.
if (parts.length != 2) continue;
highReturns.add(new HighScore(parts[0], Integer.parseInt(parts[1]) ) );
}
}
return highReturns;
}
private static void writeScores(Path scoreFile, HighScore[] scores, int scoreSize) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(scoreFile, Charset.forName("UTF-8") ) ) {
for (int i = 0; i < scoreSize; ++i) {
String writeoutLine = scores[i].getName() + "," + scores[i].getScore() + "\n";
writer.write(writeoutLine);
}
}
}
/**
*
* Constructs an instance of this object from a high score .txt, If this file cannot be
* read, the generated high scores will be empty. If the file does not exist, a new file
* will be generated and an empty high scores object will be returned.
*
* @return
* instance of this object
*
*/
public static HighScores fromFile(Path highScoreList) {
try {
if (!(Files.exists(highScoreList) ) ) {
Files.createFile(highScoreList);
}
List<HighScore> tempScores = readScores(highScoreList);
return new HighScores(tempScores);
} catch (IOException e) {
LOGGER.log(Level.WARNING,
"Could not read high scores: " + e.getMessage(),
e);
return new HighScores();
}
}
public static final class HighScore implements Comparable<HighScore> {
private final String name;
private final int score;
private HighScore(final String name, final int score) { this.name = name; this.score = score; }
public String getName() { return name; }
public int getScore() { return score; }
/** Comparison is on score only. */
@Override public int compareTo(HighScore o) {
// We invert the logic and return -1 if we are greater to force descending order sort.
return o.score - this.score;
}
}
}